#pragma once

#include "GlobalException.h"
#include "Logger.h"

#include "includeCL.h"

#include <fstream>
#include <iostream>
#include <string>



#include <boost/shared_ptr.hpp>
#include <boost/scoped_ptr.hpp>

class OpenClTemplate
{
	OpenClTemplate(
		int _deviceType,
		std::string _kernelFile,
		std::string _buildOptions
		)
		:
		deviceType(_deviceType ),
		kernelFile(_kernelFile),
		buildOptions(_buildOptions)
	{
		init();
	}


	OpenClTemplate(const OpenClTemplate&);
	OpenClTemplate& operator=(const OpenClTemplate&);
public:
	int deviceType;
	std::string kernelFile;
	std::string buildOptions;

	cl::vector< cl::Platform > platformList;
	cl::Context context;
	cl::vector<cl::Device> devices;
	cl::Device device;
	cl::CommandQueue queue;
	cl::Program::Sources source;
	cl::Program program;

	cl_ulong cachedCL_DEVICE_GLOBAL_MEM_SIZE;
	cl_ulong cachedCL_DEVICE_LOCAL_MEM_SIZE;
	cl_ulong cachedCL_DEVICE_MAX_CONSTANT_BUFFER_SIZE;
	cl_ulong cachedCL_DEVICE_MAX_MEM_ALLOC_SIZE;
	size_t cachedCL_DEVICE_MAX_WORK_GROUP_SIZE;
	size_t cachedCL_DEVICE_MAX_WORK_ITEM_DIMENSIONS;
	size_t cachedCL_DEVICE_MAX_WORK_ITEM_SIZES[3];
	size_t cachedCL_DEVICE_MAX_COMPUTE_UNITS;

	size_t cachedCL_KERNEL_PREFERRED_WORK_GROUP_SIZE_MULTIPLE;
	size_t cachedCL_KERNEL_WORK_GROUP_SIZE;
	cl_ulong cachedCL_KERNEL_LOCAL_MEM_SIZE;
	cl_ulong cachedCL_KERNEL_PRIVATE_MEM_SIZE;



public:
	typedef boost::shared_ptr<OpenClTemplate> SharedPtr;

	static SharedPtr constructDefault(std::string _kernelFile, std::string _buildOptions)
	{
		return SharedPtr(
			new OpenClTemplate(
			CL_DEVICE_TYPE_GPU,
			_kernelFile,
			_buildOptions
			)
			);
	}


	static SharedPtr constructCPU(std::string _kernelFile, std::string _buildOptions)
	{
		return SharedPtr(
			new OpenClTemplate(
			CL_DEVICE_TYPE_GPU,
			_kernelFile,
			_buildOptions
			)
			);
	}

	~OpenClTemplate()
	{
	};

	void init()
	{
		    
		cl::Platform::get(&platformList);
    
    
		cl_context_properties cprops[3] = 
		{CL_CONTEXT_PLATFORM, 
			(cl_context_properties)((platformList[0])()), 0};
 
		context= cl::Context(deviceType, cprops);
			
    
		devices = context.getInfo<CL_CONTEXT_DEVICES>();
    
		device=devices[0];
		queue=cl::CommandQueue(context, device, 
					CL_QUEUE_PROFILING_ENABLE);

		std::ifstream file(kernelFile);
		std::string prog(std::istreambuf_iterator<char>(file),
			(std::istreambuf_iterator<char>()));
		source= cl::Program::Sources( 1, std::make_pair(prog.c_str(),
							  prog.length()+1));
		program=cl::Program(context, source);
		file.close();
 
		try 
		{
			program.build(devices,buildOptions.c_str());
		} 
		catch(cl::Error& err) 
		{
			// Get the build log
			Logger::getOStream()<<std::endl << "Build failed! " << err.what() 
			<< '(' << err.err() << ')';

			Logger::getOStream()<<std::endl <<"Build options:" <<buildOptions;

			Logger::getOStream()<<std::endl << "retrieving  log ... " << std::endl;
			Logger::getOStream() 
			<< program.getBuildInfo<CL_PROGRAM_BUILD_LOG>(devices[0]);
			throw;
		}


		cachedCL_DEVICE_GLOBAL_MEM_SIZE=device.getInfo<CL_DEVICE_GLOBAL_MEM_SIZE>();
		cachedCL_DEVICE_LOCAL_MEM_SIZE=device.getInfo<CL_DEVICE_LOCAL_MEM_SIZE>();
		cachedCL_DEVICE_MAX_CONSTANT_BUFFER_SIZE=device.getInfo<CL_DEVICE_MAX_CONSTANT_BUFFER_SIZE>();
		cachedCL_DEVICE_MAX_MEM_ALLOC_SIZE=device.getInfo<CL_DEVICE_MAX_MEM_ALLOC_SIZE>();
		cachedCL_DEVICE_MAX_WORK_GROUP_SIZE=device.getInfo<CL_DEVICE_MAX_WORK_GROUP_SIZE>();
		cachedCL_DEVICE_MAX_WORK_ITEM_DIMENSIONS=device.getInfo<CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS>();
		device.getInfo(CL_DEVICE_MAX_WORK_ITEM_SIZES,&cachedCL_DEVICE_MAX_WORK_ITEM_SIZES);
		cachedCL_DEVICE_MAX_COMPUTE_UNITS=device.getInfo<CL_DEVICE_MAX_COMPUTE_UNITS>();
		


		Logger::getOStream()<<std::endl << "cachedCL_DEVICE_GLOBAL_MEM_SIZE="<<cachedCL_DEVICE_GLOBAL_MEM_SIZE;
		Logger::getOStream()<<std::endl << "cachedCL_DEVICE_LOCAL_MEM_SIZE="<<cachedCL_DEVICE_LOCAL_MEM_SIZE;
		Logger::getOStream()<<std::endl << "cachedCL_DEVICE_MAX_CONSTANT_BUFFER_SIZE="<<cachedCL_DEVICE_MAX_CONSTANT_BUFFER_SIZE;
		Logger::getOStream()<<std::endl << "cachedCL_DEVICE_MAX_MEM_ALLOC_SIZE="<<cachedCL_DEVICE_MAX_MEM_ALLOC_SIZE;
		Logger::getOStream()<<std::endl << "cachedCL_DEVICE_MAX_WORK_GROUP_SIZE="<<cachedCL_DEVICE_MAX_WORK_GROUP_SIZE;
		Logger::getOStream()<<std::endl << "cachedCL_DEVICE_MAX_WORK_ITEM_DIMENSIONS="<<cachedCL_DEVICE_MAX_WORK_ITEM_DIMENSIONS;
		Logger::getOStream()<<std::endl << "cachedCL_DEVICE_MAX_WORK_ITEM_SIZES[0]="<<cachedCL_DEVICE_MAX_WORK_ITEM_SIZES[0];
		Logger::getOStream()<<std::endl << "cachedCL_DEVICE_MAX_WORK_ITEM_SIZES[1]="<<cachedCL_DEVICE_MAX_WORK_ITEM_SIZES[1];
		Logger::getOStream()<<std::endl << "cachedCL_DEVICE_MAX_WORK_ITEM_SIZES[2]="<<cachedCL_DEVICE_MAX_WORK_ITEM_SIZES[2];
		Logger::getOStream()<<std::endl << "cachedCL_DEVICE_MAX_COMPUTE_UNITS="<<cachedCL_DEVICE_MAX_COMPUTE_UNITS;


	}

	cl::Kernel buildKernel(std::string _functionName)
	{
		cl::Kernel kernel=cl::Kernel(program, _functionName.c_str());
				cachedCL_KERNEL_PREFERRED_WORK_GROUP_SIZE_MULTIPLE =kernel.getWorkGroupInfo<CL_KERNEL_PREFERRED_WORK_GROUP_SIZE_MULTIPLE>(this->device);
				cachedCL_KERNEL_WORK_GROUP_SIZE=kernel.getWorkGroupInfo<CL_KERNEL_WORK_GROUP_SIZE>(this->device);
				cachedCL_KERNEL_LOCAL_MEM_SIZE=kernel.getWorkGroupInfo<CL_KERNEL_LOCAL_MEM_SIZE>(this->device);
				cachedCL_KERNEL_PRIVATE_MEM_SIZE=kernel.getWorkGroupInfo<CL_KERNEL_PRIVATE_MEM_SIZE>(this->device);

				Logger::getOStream()<<std::endl<<"_functionName: "<<_functionName;
				Logger::getOStream()<<std::endl << "cachedCL_KERNEL_PREFERRED_WORK_GROUP_SIZE_MULTIPLE="<<cachedCL_KERNEL_PREFERRED_WORK_GROUP_SIZE_MULTIPLE;
				Logger::getOStream()<<std::endl << "cachedCL_KERNEL_WORK_GROUP_SIZE="<<cachedCL_KERNEL_WORK_GROUP_SIZE;
				Logger::getOStream()<<std::endl << "cachedCL_KERNEL_LOCAL_MEM_SIZE="<<cachedCL_KERNEL_LOCAL_MEM_SIZE;
				Logger::getOStream()<<std::endl << "cachedCL_KERNEL_PRIVATE_MEM_SIZE="<<cachedCL_KERNEL_PRIVATE_MEM_SIZE;

		return kernel;
	}

	double waitForKernelExecution(cl::Event& event)
	{
		event.wait();
		cl_ulong start=
		  event.getProfilingInfo<CL_PROFILING_COMMAND_START>();
		cl_ulong end=
		  event.getProfilingInfo<CL_PROFILING_COMMAND_END>();
		double time = 1.e-9 * (end-start);
		return time;
	}


	static void exampleUse()
	{
		SharedPtr spOpenClTemplate=
			constructDefault("OpenClTemplate.cl","");

		//________________________________________
		cl_uint width=256;
		cl_uint multiplier=2;



		cl_uint maxDims;
		size_t globalThreads[1];
		size_t localThreads[1];
		size_t maxWorkGroupSize;
		size_t maxWorkItemSizes[3];

		/**
		* Query device capabilities. Maximum 
		* work item dimensions and the maximmum
		* work item sizes
		*/ 
		maxWorkGroupSize=spOpenClTemplate->device.getInfo<CL_DEVICE_MAX_WORK_GROUP_SIZE>();
		maxDims=spOpenClTemplate->device.getInfo<CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS>();
		spOpenClTemplate->device.getInfo(CL_DEVICE_MAX_WORK_ITEM_SIZES,&maxWorkItemSizes);


    
		globalThreads[0] = width;
		localThreads[0]  = 1;

		if(globalThreads[0] > maxWorkItemSizes[0] || 
			localThreads[0] > maxWorkGroupSize)
		{
			Logger::getOStream()<<std::endl<<"Warning: Device does not support requested number of work items.";
		}

		cl_uint sizeInBytes = width * sizeof(cl_uint);
		boost::scoped_ptr<cl_uint> input(new cl_uint[sizeInBytes]);

		for(cl_uint i = 0; i < width; i++)
        input.get()[i] = i;

		boost::scoped_ptr<cl_uint> output(new cl_uint[sizeInBytes]);


		for(cl_uint i = 0; i < width; i++)
			input.get()[i] = i;

		//_________________________________________

		

		cl::Kernel kernel=spOpenClTemplate->buildKernel("templateKernel");

		cl::Buffer inputBuffer(
			spOpenClTemplate->context, 
            CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR,
            sizeof(cl_uint) * width,
            input.get());

		cl::Buffer outputBuffer(
					   spOpenClTemplate->context, 
                       CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR,
                       sizeof(cl_uint) * width,
                       output.get());

		kernel.setArg(0, outputBuffer);
		kernel.setArg(1, inputBuffer);
		kernel.setArg(2, multiplier);

		cl::Event event;


		spOpenClTemplate->queue.enqueueNDRangeKernel
			(
			kernel,
			cl::NullRange,
			cl::NDRange(globalThreads[0]),
			cl::NDRange(localThreads[0]),
			NULL,
			&event);

		spOpenClTemplate->waitForKernelExecution(event);

		spOpenClTemplate->queue.enqueueReadBuffer(outputBuffer,CL_TRUE,0,width * sizeof(cl_uint),output.get());

		bool resultOk=true;
		for(cl_uint i = 0; i < width; i++)
		{
			if( output.get()[i]!=input.get()[i]*multiplier )resultOk=false;
		}


		if(!resultOk)
			throw GlobalException("Invalid output");


	}

	
	static void exampleUseCPU()
	{
		SharedPtr spOpenClTemplate=
			constructCPU("OpenClTemplate.cl","");

		//________________________________________
		cl_uint width=256;
		cl_uint multiplier=2;



		cl_uint maxDims;
		size_t globalThreads[1];
		size_t localThreads[1];
		size_t maxWorkGroupSize;
		size_t maxWorkItemSizes[3];

		/**
		* Query device capabilities. Maximum 
		* work item dimensions and the maximmum
		* work item sizes
		*/ 
		maxWorkGroupSize=spOpenClTemplate->device.getInfo<CL_DEVICE_MAX_WORK_GROUP_SIZE>();
		maxDims=spOpenClTemplate->device.getInfo<CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS>();
		spOpenClTemplate->device.getInfo(CL_DEVICE_MAX_WORK_ITEM_SIZES,&maxWorkItemSizes);


    
		globalThreads[0] = width;
		localThreads[0]  = 1;

		if(globalThreads[0] > maxWorkItemSizes[0] || 
			localThreads[0] > maxWorkGroupSize)
		{
			Logger::getOStream()<<std::endl<<"Warning: Device does not support requested number of work items.";
		}

		cl_uint sizeInBytes = width * sizeof(cl_uint);
		boost::scoped_ptr<cl_uint> input(new cl_uint[sizeInBytes]);

		for(cl_uint i = 0; i < width; i++)
        input.get()[i] = i;

		boost::scoped_ptr<cl_uint> output(new cl_uint[sizeInBytes]);


		for(cl_uint i = 0; i < width; i++)
			input.get()[i] = i;

		//_________________________________________

		

		cl::Kernel kernel=spOpenClTemplate->buildKernel("templateKernel");

		cl::Buffer inputBuffer(
			spOpenClTemplate->context, 
            CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR,
            sizeof(cl_uint) * width,
            input.get());

		cl::Buffer outputBuffer(
					   spOpenClTemplate->context, 
                       CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR,
                       sizeof(cl_uint) * width,
                       output.get());

		kernel.setArg(0, outputBuffer);
		kernel.setArg(1, inputBuffer);
		kernel.setArg(2, multiplier);

		cl::Event event;


		spOpenClTemplate->queue.enqueueNDRangeKernel
			(
			kernel,
			cl::NullRange,
			cl::NDRange(globalThreads[0]),
			cl::NDRange(localThreads[0]),
			NULL,
			&event);

		spOpenClTemplate->waitForKernelExecution(event);

		spOpenClTemplate->queue.enqueueReadBuffer(outputBuffer,CL_TRUE,0,width * sizeof(cl_uint),output.get());

		bool resultOk=true;
		for(cl_uint i = 0; i < width; i++)
		{
			if( output.get()[i]!=input.get()[i]*multiplier )resultOk=false;
		}


		if(!resultOk)
			throw GlobalException("Invalid output");


	}

	

};